第7回 Springの宣言的トランザクションのしくみ【AOP】
よく訓練されたアップル信者、都元です。長雨が続いたと思ったらこの晴れ間。夏っすね。止まってしまわないうちにガンガン書いて行こうと思います。
Springによるトランザクションの実現方法を理解する
さて前回、@Transactional
というアノテーションを付与することにより、宣言的にトランザクション制御を記述できることを学びました。
@Autowired UserRepository userRepos; @Transactional public void execute() { // 成功するDB書き込み操作 userRepos.save(new User("torazuka", "$2a$10$fx33wHST4ecwp53MB5QvROQtIYwkdCU2O3XJK6LuCmm415dRncluC")); // からの失敗 throw new RuntimeException(); }
つまり、@Transactional
が付いたメソッドを呼ぶ前後に、トランザクションの開始やコミットの処理が黒魔術として動いているわけです。このようにメソッド呼び出しの前後等に一般化した共通処理を差し込むプラグラミング方式をAOP(Aspect oriented programming=アスペクト指向プログラミング)と呼びます。
さて実際この時、何が起こっているのか、その一端を見てみましょう。context.getBean
の直後、System.out.println(main);
でインスタンスの詳細を覗き見してみます。この結果は
jp.classmethod.example.berserker.DataAccessSample@5167f57d
という感じでDataAccessSample
クラスのインスタンスであることを期待したと思いますが、実際は
jp.classmethod.example.berserker.DataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238@5167f57d
などという結果が帰ってきます。DataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238
がクラス名です。これはプロキシと呼ばれるインスタンスです。
BerserkerApplication
クラスのインスタンスに対して素朴にexecute
メソッドを呼んだ場合、@Transactional
の有無にかかわらず、素朴にメソッドの中身が実行されてしまい、トランザクション制御どころではありません。
そうではなく、例えばこのような派生クラスを作ることによって、execute
本体を実行する前後に任意の処理を挟み込むことが可能になりますね。
public class DataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238 extends BerserkerApplication { private BerserkerApplication delegate; public BerserkerApplication$$EnhancerBySpringCGLIB$$e8b1a238(BerserkerApplication delegate) { this.delegate = delegate; } @Overwrite public void execute() { // 前 delegate.execute(); // 後 } }
そしてDataAccessSample$$EnhancerBySpringCGLIB$$e8b1a238
のインスタンスはDataAccessSample
を継承しているため、利用者はあたかもDataAccessSample
を素で触っているような錯覚を起こしながら、これらの機能を享受できる、という仕組みです。
「いや、でも俺DataAccessSample
の派生クラスなんて作ってないし」と思うかもしれません。素朴なJavaの世界では、自分の作ったクラスの派生クラスは、自分が書かない限り存在しないはずでした。しかし、cglib(Code Generation Library)等のJavaライブラリを使えば、(コンパイル時ではなく)実行時に新しいクラスを作り出し、そのインスタンスを生成したりできます。
これが黒魔術の正体です。
プロキシによるAOPの弱点
さて、次のようなコードを実行してみます。
public void execute() { execute0(); } @Transactional private void execute0() { // 成功するDB書き込み操作 userRepos.save(new User("torazuka", "$2a$10$fx33wHST4ecwp53MB5QvROQtIYwkdCU2O3XJK6LuCmm415dRncluC")); }
結果はUserRepository#save
内での例外となりました。
java.lang.IllegalStateException: It seems not to be existing a transaction.
前提として、UserRepository#save
メソッドはトランザクションの中でしか呼べません。例えば@Transactional
の付け忘れなど、トランザクション外で呼ぶと、このような例外が発生します。これはSpring連携した際のMirageの制限事項です。さて今回はきちんとアノテーションを付けているはずなのに、なぜトランザクションの中にいないと怒られてしまったのでしょうか。
落ち着いて、頭の中でこのクラスのサブクラスを作って、execute0
メソッドの@Transactional
処理をどのように実行時に実現するのか想像してみてください。……privateメソッドのオーバーライドはできない!? ということに気づきましたか?
ではこれがpublicであれば問題ないのでしょうか。さらに想像してみてください。proxyのexecute0がオーバーライドされていても…
- proxyのexecuteが呼ばれる
- delegateのexecuteが呼ばれる
- delegateのexecute0が呼ばれる
- userReposのsaveが呼ばれる
おっと、proxyのexecute0が呼ばれるタイミングが無く、トランザクションの魔法は掛かりません。
これがプロキシによるAOPの弱点です。
まとめ
この弱点は、AOPを「プロキシによって」ではなく、「ウィービング(織り込み)によって」実現することによって克服できたりするようです。ウィービングはAspectJと呼ばれるあれやこれやを使います。しかしひとまず私自身は、この弱点を理解して付き合っていくという方針を取っており、AspectJで頑張ったことがありません。
これまでの経験上、「アスペクトの掛かったメソッドを自分自身のクラスの中から呼ぶ」という行為にさえ気をつけていれば(たまに不便なこともありますが)ほとんどの問題は回避可能です。